module jinji.util.string;

import jinji.common.type,
jinji.config,
std.range,
std.traits,
tame.string;
import tame.conv : text, intToStr;
import std.typecons : isBitFlagEnum;

public import tame.string : lastIndexOf;
public import tame.windows.charset;

/// 判断是否为file:链接
auto isFileURL(string url) {
	return url.startsWith("file://");
}

/// 验证链接
void validateURL(string url) {
	import jinji.config,
	jinji.util.error;

	checkArgRange(url.stringof, url.length <= maxURLLength, "链接过长");
	checkArg(url.stringof, url.startsWith("http://") ||
			url.startsWith("https://") ||
			url.isFileURL ||
			url.startsWith("data:"), text("无效的链接'", url, "'"));
}

/// 允许的字符
enum Allow {
	none,
	/// 空字符串
	empty = 1 << 0,
	/// 非打印字符
	nonPrintable = 1 << 1,
	/// 开头空白字符
	leadingWhiteSpace = 1 << 2,
	/// 结尾空白字符
	tailingWhiteSpace = 1 << 3,
	/// 换行符
	line = 1 << 4,
}

/// 验证文本
Desc parseText(string name)(string s,
	uint maxLen = 100,
	BitFlags!Allow allow = Allow.none) {
	import jinji.util.error;
	import std.ascii;

	uint len;
	dchar last;
	foreach (dchar c; s) {
		if (!allow.line)
			checkArg(s.stringof, c != '\n', name ~ "包含换行符");
		if (last == dchar.init && !allow.leadingWhiteSpace)
			checkArg(s.stringof, !isWhite(c), name ~ "开头包含空白字符");
		if (!allow.nonPrintable)
			checkArg(s.stringof, c >= ' ' || c == '\t', name ~ "包含非法字符");
		len++;
		last = c;
	}
	if (!allow.tailingWhiteSpace)
		checkArg(s.stringof, !isWhite(last), name ~ "结尾包含空白字符");
	if (!allow.empty)
		checkArgRange(s.stringof, len != 0, name ~ "不能为空");
	checkArgRange(s.stringof, len <= maxLen, name ~ "过长");
	return cast(Desc)s;
}

/// 验证ID
Id parseId(long id) {
	import jinji.util.error;

	checkArg("id", id >= 0, "ID不能为负数");
	return cast(Id)id;
}

/// 验证名称
Name parseName(string s)
	=> cast(Name)parseText!"名称"(s, 20);

/// 验证描述
Desc parseDesc(string s)
	=> cast(Desc)parseText!"描述"(s, maxDescLength, BitFlags!Allow(Allow.line | Allow.empty));

pragma(inline, true) T trust(T = string)(string s)
	=> cast(T)s;

struct BitFlags(E, bool unsafe = false) if (unsafe || isBitFlagEnum!E) {
	import std.traits : OriginalType;
	import std.meta : allSatisfy;

@safe @nogc pure nothrow:
private:
	enum isBaseEnumType(T) = is(E == T);
	alias Base = OriginalType!E;

public:
	Base value;
	alias value this;
	this(E flag) {
		this = flag;
	}

	this(T...)(T flags) if (allSatisfy!(isBaseEnumType, T)) {
		this = flags;
	}

	bool opCast(B : bool)() const => value != 0;

	auto opUnary(string op)() const
	if (op == "~") {
		return BitFlags(cast(E)cast(Base)~value);
	}

	auto ref opAssign(T...)(T flags) if (allSatisfy!(isBaseEnumType, T)) {
		value = 0;
		foreach (E flag; flags) {
			value |= flag;
		}
		return this;
	}

	auto ref opAssign(E flag) {
		value = flag;
		return this;
	}

	auto ref opOpAssign(string op : "|")(BitFlags flags) {
		value |= flags.value;
		return this;
	}

	auto ref opOpAssign(string op : "&")(BitFlags flags) {
		value &= flags.value;
		return this;
	}

	auto ref opOpAssign(string op : "|")(E flag) {
		value |= flag;
		return this;
	}

	auto ref opOpAssign(string op : "&")(E flag) {
		value &= flag;
		return this;
	}

	auto opBinary(string op)(BitFlags flags) const
	if (op == "|" || op == "&") {
		BitFlags result = this;
		result.opOpAssign!op(flags);
		return result;
	}

	auto opBinary(string op)(E flag) const
	if (op == "|" || op == "&") {
		BitFlags result = this;
		result.opOpAssign!op(flag);
		return result;
	}

	auto opBinaryRight(string op)(E flag) const
	if (op == "|" || op == "&") {
		return opBinary!op(flag);
	}

	bool opDispatch(string name)() const
	if (__traits(hasMember, E, name)) {
		enum e = __traits(getMember, E, name);
		return (value & e) == e;
	}

	void opDispatch(string name)(bool set) if (__traits(hasMember, E, name)) {
		enum e = __traits(getMember, E, name);
		if (set)
			value |= e;
		else
			value &= ~e;
	}
}

/// Set values with the | operator and test with &
@safe @nogc pure nothrow unittest {
	enum Enum {
		A = 1 << 0,
	}

	// A default constructed BitFlags has no value set
	immutable BitFlags!Enum flags_empty;
	assert(!flags_empty.A);

	// Value can be set with the | operator
	immutable flags_A = flags_empty | Enum.A;

	// and tested using property access
	assert(flags_A.A);

	// or the & operator
	assert(flags_A & Enum.A);
	// which commutes.
	assert(Enum.A & flags_A);
}

/// A default constructed BitFlags has no value set
@safe @nogc pure nothrow unittest {
	enum Enum {
		None,
		A = 1 << 0,
		B = 1 << 1,
		C = 1 << 2
	}

	immutable BitFlags!Enum flags_empty;
	assert(!(flags_empty & (Enum.A | Enum.B | Enum.C)));
	assert(!(flags_empty & Enum.A) && !(flags_empty & Enum.B) && !(flags_empty & Enum.C));
}

// BitFlags can be variadically initialized
@safe @nogc pure nothrow unittest {
	import std.traits : EnumMembers;

	enum Enum {
		A = 1 << 0,
		B = 1 << 1,
		C = 1 << 2
	}

	// Values can also be set using property access
	BitFlags!Enum flags;
	flags.A = true;
	assert(flags & Enum.A);
	flags.A = false;
	assert(!(flags & Enum.A));

	// BitFlags can be variadically initialized
	immutable BitFlags!Enum flags_AB = BitFlags!Enum(Enum.A, Enum.B);
	assert(flags_AB.A && flags_AB.B && !flags_AB.C);

	// You can use the EnumMembers template to set all flags
	immutable BitFlags!Enum flags_all = EnumMembers!Enum;
	assert(flags_all.A && flags_all.B && flags_all.C);
}

/// Binary operations: subtracting and intersecting flags
@safe @nogc pure nothrow unittest {
	enum Enum {
		A = 1 << 0,
		B = 1 << 1,
		C = 1 << 2,
	}

	immutable BitFlags!Enum flags_AB = BitFlags!Enum(Enum.A, Enum.B);
	immutable BitFlags!Enum flags_BC = BitFlags!Enum(Enum.B, Enum.C);

	// Use the ~ operator for subtracting flags
	immutable BitFlags!Enum flags_B = flags_AB & ~BitFlags!Enum(Enum.A);
	assert(!flags_B.A && flags_B.B && !flags_B.C);

	// use & between BitFlags for intersection
	assert(flags_B == (flags_BC & flags_AB));
}

/// All the binary operators work in their assignment version
@safe @nogc pure nothrow unittest {
	enum Enum {
		A = 1 << 0,
		B = 1 << 1,
	}

	BitFlags!Enum flags_empty, temp, flags_AB;
	flags_AB = Enum.A | Enum.B;

	temp |= flags_AB;
	assert(temp == (flags_empty | flags_AB));

	temp = flags_empty;
	temp |= Enum.B;
	assert(temp == (flags_empty | Enum.B));

	temp = flags_empty;
	temp &= flags_AB;
	assert(temp == (flags_empty & flags_AB));

	temp = flags_empty;
	temp &= Enum.A;
	assert(temp == (flags_empty & Enum.A));
}

/// Conversion to bool and int
@safe @nogc pure nothrow unittest {
	enum Enum {
		A = 1 << 0,
		B = 1 << 1,
	}

	BitFlags!Enum flags;

	// BitFlags with no value set evaluate to false
	assert(!flags);

	// BitFlags with at least one value set evaluate to true
	flags |= Enum.A;
	assert(flags);

	// This can be useful to check intersection between BitFlags
	BitFlags!Enum flags_AB = Enum.A | Enum.B;
	assert(flags & flags_AB);
	assert(flags & Enum.A);

	// You can of course get you raw value out of flags
	auto value = cast(int)flags;
	assert(value == Enum.A);
}

/// You need to specify the `unsafe` parameter for enums with custom values
@safe @nogc pure nothrow unittest {
	enum UnsafeEnum {
		A = 1,
		B = 2,
		C = 4,
		BC = B | C
	}

	static assert(!__traits(compiles, { BitFlags!UnsafeEnum flags; }));
	BitFlags!(UnsafeEnum, Yes.unsafe) flags;

	// property access tests for exact match of unsafe enums
	flags.B = true;
	assert(!flags.BC); // only B
	flags.C = true;
	assert(flags.BC); // both B and C
	flags.B = false;
	assert(!flags.BC); // only C

	// property access sets all bits of unsafe enum group
	flags = flags.init;
	flags.BC = true;
	assert(!flags.A && flags.B && flags.C);
	flags.A = true;
	flags.BC = false;
	assert(flags.A && !flags.B && !flags.C);
}

// Negation of BitFlags should work with any base type.
// Double-negation of BitFlags should work.
@safe @nogc pure nothrow unittest {
	static foreach (alias Base; Seq!(
			byte,
			ubyte,
			short,
			ushort,
			int,
			uint,
			long,
			ulong,
		)) {
		{
			enum Enum : Base {
				A = 1 << 0,
				B = 1 << 1,
				C = 1 << 2,
			}

			auto flags = BitFlags!Enum(Enum.A);

			assert(flags == ~~flags);
		}
	}
}

/++
正则表达式还原

其中输入的replacement不能包含'\0'字符
+/
bool revertRegex(S)(ref S pattern, ref S replacement) @trusted
if (isInputRange!S && isSomeChar!(ElementEncodingType!S)) {
	alias C = Unqual!(ElementEncodingType!S);
	import dast.util,
	std.array,
	std.regex,
	std.string;

	alias str = pattern;
	auto r = appender!(C[]);
	foreach (c; replacement.escaper)
		r ~= c;

	C[] r = r[].replace(`\$`, "$").replace("$$", "$\0");
	r = appender!(C[]);
	uint i, len = cast(uint)str.length;
	ulong index = 0;
	ulong[] stack;
	char[11] buf = void;
	buf[0] = '$';
	for (; i < len; i++) {
		C c = str[i];
		if (c == '(') {
			if (i + 1 == str.length)
				return false;

			if (str[i + 1] == '?') {
				++i;
				stack ~= uint.max;
				continue;
			}
			stack ~= i | ++index << 32;
		} else if (c == ')') {
			if (!stack.length)
				return false;

			const pos = cast(uint)stack[$ - 1],
			level = stack[$ - 1] >> 32;
			stack.popBack();
			if (pos < uint.max) {
				auto l = intToStr(buf.ptr + 1, level);
				auto s = buf[0 .. l + 1];
				if (!stack.length)
					r ~= s;
				r = r.replace(s, "(" ~ str[pos + 1 .. i].removeBrackets ~ ")");
			}
		} else if (!stack.length)
			r ~= c;
	}
	pattern = cast(S)r.replace("$\0", `\$`);
	replacement = cast(S)r[];
	return true;
}

unittest {
	auto pattern = `gh:([\w\-]+)/([\w\-\.]+)`;
	auto replacement = `https://github.com/$1/$2`;
	assert(revertRegex(pattern, replacement));
	assert(pattern == `https\:\/\/github\.com\/([\w\-]+)\/([\w\-\.]+)`, pattern);
	assert(replacement == `gh:$1/$2`, replacement);
	// 括号嵌套
	pattern = "((a)b)";
	replacement = "$1 $2";
	assert(revertRegex(pattern, replacement));
	assert(pattern == `(ab) (a)`, pattern);
	assert(replacement == `$1`, replacement);
}

/++
删除字符串中未转义的括号
+/
S removeBrackets(S)(S s, ptrdiff_t start = 0)
if (isInputRange!S && isSomeChar!(ElementType!S)) {
	alias Char = Unqual!(ElementType!S);
	import std.array;

	if (start < 0)
		start += s.length;
	auto r = appender!(char[]);
	for (size_t i = start; i < s.length; i++) {
		Char c = s[i];
		if (c == '\\') {
			r ~= c;
			if (i + 1 < s.length)
				r ~= s[++i];
		} else if (c != '(' && c != ')')
			r ~= c;
	}
	return cast(S)r[];
}

unittest {
	assert(removeBrackets(`(\(foo)bar)\)`) == `\(foobar\)`);
}
